"""
Extended Room

Evennia Contribution - Griatch 2012

This is an extended Room typeclass for Evennia. It is supported
by an extended Look command and an extended @desc command, also
in this module.


Features:

1) Time-changing description slots

This allows to change the full description text the room shows
depending on larger time variations. Four seasons - spring, summer,
autumn and winter are used by default). The season is calculated
on-demand (no Script or timer needed) and updates the full text block.

There is also a general description which is used as fallback if
one or more of the seasonal descriptions are not set when their
time comes.

An updated @desc command allows for setting seasonal descriptions.


2) In-description changing tags

Within each seasonal (or general) description text, you can also embed
time-of-day dependent sections. Text inside such a tag will only show
during that particular time of day. The tags looks like <timeslot> ...
</timeslot>. By default there are four timeslots per day - morning,
afternoon, evening and night.


3) Details

The Extended Room can be "detailed" with special keywords. This makes
use of a special Look command. Details are "virtual" targets to look
at, without there having to be a database object created for it. The
Details are simply stored in a dictionary on the room and if the look
command cannot find an object match for a a "look <target>" command it
will also look through the available details at the current location
if applicable. An extended @desc command is used to set details.


4) Extra commands

  CmdExtendedLook - look command supporting room details
  CmdExtendedDesc - @desc command allowing to add seasonal descs and details,
                    as well as listing them
  CmdGameTime     - A simple "time" command, displaying the current time and season.


Installation/testing:

1) Add CmdExtendedLook, CmdExtendedDesc and CmdGameTime to the default cmdset (see wiki how to do this).
2) @dig a room of type contrib.extended_room.ExtendedRoom (or make it the default room type)
3) Use @desc and @detail to customize the room, then play around!

"""

import re
from django.conf import settings
from ev import Room
from ev import gametime
from ev import default_cmds
from ev import utils

# error return function, needed by Extended Look command
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))

# regexes for in-desc replacements
RE_MORNING = re.compile(r"<morning>(.*?)</morning>", re.IGNORECASE)
RE_AFTERNOON = re.compile(r"<afternoon>(.*?)</afternoon>", re.IGNORECASE)
RE_EVENING = re.compile(r"<evening>(.*?)</evening>", re.IGNORECASE)
RE_NIGHT = re.compile(r"<night>(.*?)</night>", re.IGNORECASE)
# this map is just a faster way to select the right regexes (the first
# regex in each tuple will be parsed, the following will always be weeded out)
REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
            "afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
            "evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
            "night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING)}

# set up the seasons and time slots. This assumes gametime started at the
# beginning of the year (so month 1 is equivalent to January), and that
# one CAN divive the game's year into four seasons in the first place ...
MONTHS_PER_YEAR = settings.TIME_MONTH_PER_YEAR
SEASONAL_BOUNDARIES = (3/12.0, 6/12.0, 9/12.0)
HOURS_PER_DAY = settings.TIME_HOUR_PER_DAY
DAY_BOUNDARIES = (0, 6/24.0, 12/24.0, 18/24.0)

# implements the Extended Room

class ExtendedRoom(Room):
    """
    This room implements a more advanced look functionality depending on time. It also
    allows for "details", together with a slightly modified look command.
    """
    def at_object_creation(self):
        "Called when room is first created only."
        self.db.spring_desc = ""
        self.db.summer_desc = ""
        self.db.autumn_desc = ""
        self.db.winter_desc = ""
        # the general desc is used as a fallback if a given seasonal one is not set
        self.db.general_desc = ""
        self.db.raw_desc = "" # will be set dynamically. Can contain raw timeslot codes
        self.db.desc = "" # this will be set dynamically at first look. Parsed for timeslot codes
        # these will be filled later
        self.ndb.last_season = None
        self.ndb.last_timeslot = None
        # detail storage
        self.db.details = {}

    def get_time_and_season(self):
        """
        Calcualte the current time and season ids
        """
        # get the current time as parts of year and parts of day
        time = gametime.gametime(format=True) # returns a tuple (years,months,weeks,days,hours,minutes,sec)
        month, hour = time[1], time[4]
        season = float(month) / MONTHS_PER_YEAR
        timeslot = float(hour) / HOURS_PER_DAY

        # figure out which slots these represent
        if SEASONAL_BOUNDARIES[0] <= season < SEASONAL_BOUNDARIES[1]: curr_season = "spring"
        elif SEASONAL_BOUNDARIES[1] <= season < SEASONAL_BOUNDARIES[2]: curr_season = "summer"
        elif SEASONAL_BOUNDARIES[2] <= season < 1.0 + SEASONAL_BOUNDARIES[0]: curr_season = "autumn"
        else: curr_season = "winter"

        if DAY_BOUNDARIES[0] <= timeslot < DAY_BOUNDARIES[1]: curr_timeslot = "night"
        elif DAY_BOUNDARIES[1] <= timeslot < DAY_BOUNDARIES[2]: curr_timeslot = "morning"
        elif DAY_BOUNDARIES[2] <= timeslot < DAY_BOUNDARIES[3]: curr_timeslot = "afternoon"
        else: curr_timeslot = "evening"

        return curr_season, curr_timeslot

    def replace_timeslots(self, raw_desc, curr_time):
        """
        Filter so that only time markers <timeslot>...</timeslot> of the correct timeslot
        remains in the description.
        """
        if raw_desc:
            regextuple = REGEXMAP[curr_time]
            raw_desc = regextuple[0].sub(r"\1", raw_desc)
            raw_desc = regextuple[1].sub("", raw_desc)
            raw_desc = regextuple[2].sub("", raw_desc)
            return regextuple[3].sub("", raw_desc)
        return raw_desc

    def return_detail(self, key):
        """
        This will attempt to match a "detail" to look for in the room. A detail
        is a way to offer more things to look at in a room without having to
        add new objects. For this to work, we require a custom look command that
        allows for "look <detail>" - the look command should defer to this method
        on the current location (if it exists) before giving up on finding the target.

        Details are not season-sensitive, but are parsed for timeslot markers.
        """
        detail = self.db.details.get(key.lower(), None)
        if detail:
            season, timeslot = self.get_time_and_season()
            detail = self.replace_timeslots(detail, timeslot)
            return detail
        return None

    def return_appearance(self, looker):
        "This is called when e.g. the look command wants to retrieve the description of this object."
        raw_desc = self.db.raw_desc or ""
        update = False

        # get current time and season
        curr_season, curr_timeslot = self.get_time_and_season()

        # compare with previously stored slots
        last_season = self.ndb.last_season
        last_timeslot = self.ndb.last_timeslot

        if curr_season != last_season:
            # season changed. Load new desc, or a fallback.
            if curr_season == 'spring': new_raw_desc = self.db.spring_desc
            elif curr_season == 'summer': new_raw_desc = self.db.summer_desc
            elif curr_season == 'autumn': new_raw_desc = self.db.autumn_desc
            else: new_raw_desc = self.db.winter_desc
            if new_raw_desc:
                raw_desc = new_raw_desc
            else:
                # no seasonal desc set. Use fallback
                raw_desc = self.db.general_desc
            self.db.raw_desc = raw_desc
            self.ndb.last_season = curr_season
            update = True

        if curr_timeslot != last_timeslot:
            # timeslot changed. Set update flag.
            self.ndb.last_timeslot = curr_timeslot
            update = True

        if update:
            # if anything changed we have to re-parse the raw_desc for time markers
            # and re-save the description again.
            self.db.desc = self.replace_timeslots(self.db.raw_desc, curr_timeslot)
        # run the normal return_appearance method, now that desc is updated.
        return super(ExtendedRoom, self).return_appearance(looker)


# Custom Look command supporting Room details. Add this to the Default cmdset to use.

class CmdExtendedLook(default_cmds.CmdLook):
    """
    look

    Usage:
      look
      look <obj>
      look <room detail>
      look *<player>

    Observes your location, details at your location or objects in your vicinity.
    """
    def func(self):
        """
        Handle the looking - add fallback to details.
        """
        caller = self.caller
        args = self.args
        if args:
            looking_at_obj = caller.search(args, use_nicks=True, ignore_errors=True)
            if not looking_at_obj:
                # no object found. Check if there is a matching detail at location.
                location = caller.location
                if location and hasattr(location, "return_detail") and callable(location.return_detail):
                    detail = location.return_detail(args)
                    if detail:
                        # we found a detail instead. Show that.
                        caller.msg(detail)
                        return
                # no detail found. Trigger delayed error messages
                _AT_SEARCH_RESULT(caller, args, looking_at_obj, False)
                return
            else:
                # we need to extract the match manually.
                looking_at_obj = looking_at_obj[0]
        else:
            looking_at_obj = caller.location
            if not looking_at_obj:
                caller.msg("You have no location to look at!")
                return

        if not hasattr(looking_at_obj, 'return_appearance'):
            # this is likely due to us having a player instead
            looking_at_obj = looking_at_obj.character
        if not looking_at_obj.access(caller, "view"):
            caller.msg("Could not find '%s'." % args)
            return
        # get object's appearance
        caller.msg(looking_at_obj.return_appearance(caller))
        # the object's at_desc() method.
        looking_at_obj.at_desc(looker=caller)


# Custom build commands for setting seasonal descriptions and detailing extended rooms.

class CmdExtendedDesc(default_cmds.CmdDesc):
    """
    @desc - describe an object or room

    Usage:
      @desc[/switch] [<obj> =] <description>
      @detail[/del] [<key> = <description>]


    Switches for @desc:
      spring  - set description for <season> in current room
      summer
      autumn
      winter

    Switch for @detail:
      del   - delete a named detail

    Sets the "desc" attribute on an object. If an object is not given,
    describe the current room.

    The alias @detail allows to assign a "detail" (a non-object
    target for the look command) to the current room.
    """
    aliases = ["@describe", "@detail"]

    def func(self):
        "Define extended command"
        caller = self.caller
        location = caller.location
        if self.cmdstring == '@detail':
            # switch to detailing mode. This operates only on current location
            if not location:
                caller.msg("No location to detail!")
                return
            if not self.rhs:
                # no '=' used - list content of given detail
                if self.args in location.db.details:
                    string = "{wDetail '%s' on %s:\n{n" % (self.args, location)
                    string += location.db.details[self.args]
                    caller.msg(string)
                    return
            if not self.args:
                # No args given. Return all details on location
                string = "{wDetails on %s{n:\n" % location
                string += "\n".join(" {w%s{n: %s" % (key, utils.crop(text)) for key, text in location.db.details.items())
                caller.msg(string)
                return
            if self.switches and self.switches[0] in 'del':
                # removing a detail.
                if self.lhs in location.db.details:
                    del location.db.detail
                caller.msg("Detail %s deleted, if it existed." % self.lhs)
                return
            # setting a detail
            location.db.details[self.lhs] = self.rhs
            caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs))
            return
        else:
            # we are doing a @desc call
            if not self.args:
                if location:
                    string = "{wDescriptions on %s{n:\n" % location.key
                    string += " {wspring:{n %s\n" % location.db.spring_desc
                    string += " {wsummer:{n %s\n" % location.db.summer_desc
                    string += " {wautumn:{n %s\n" % location.db.autumn_desc
                    string += " {wwinter:{n %s\n" % location.db.winter_desc
                    string += " {wgeneral:{n %s" % location.db.general_desc
                    caller.msg(string)
                    return
            if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
                # a seasonal switch was given
                if self.rhs:
                    caller.msg("Seasonal descs only works with rooms, not objects.")
                    return
                switch = self.switches[0]
                if not location:
                    caller.msg("No location was found!")
                    return
                if switch == 'spring': location.db.spring_desc = self.args
                elif switch == 'summer': location.db.summer_desc = self.args
                elif switch == 'autumn': location.db.autumn_desc = self.args
                elif switch == 'winter': location.db.winter_desc = self.args
                # clear flag to force an update
                location.ndb.last_season = None
                caller.msg("Seasonal description was set on %s." % location.key)
            elif self.rhs:
                # Not a seasonal desc, and we have an =
                obj = caller.search(self.lhs)
                if not obj:
                    return
                obj.db.desc = self.rhs
                caller.msg("The description was set on %s." % obj.key)
            else:
                # set a normal non-seasonal description (fallback) on room
                obj = caller.location
                obj.db.general_desc = self.args
                obj.db.desc = self.args  # compatability
                caller.msg("General description was set on %s." % obj.key)

# Simple command to view the current time and season

class CmdGameTime(default_cmds.MuxCommand):
    """
    Check the game time

    Usage:
      time

    Shows the current in-game time and season.
    """
    key = "time"
    locks = "cmd:all()"
    help_category = "General"

    def func(self):
        "Reads time info from current room"
        location = self.caller.location
        if not location or not hasattr(location, "get_time_and_season"):
            self.caller.msg("No location available - you are outside time.")
        else:
            season, timeslot = location.get_time_and_season()
            prep = "a"
            if season == "autumn":
                prep = "an"
            self.caller.msg("It's %s %s day, in the %s." % (prep, season, timeslot))
